package transport;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;

import network.TPA;

public class TransportConnection extends Thread {
	byte incomingport;
	byte outgoingport;
	TPA in;
	TPA out;
	char ackseq;
	int seq;
	boolean timerStarted = false;
	int expectedseq;
	int sendBase;
	int lastReceivedAck;
	int resendCountdown;
	int resendCounter = 0;
	boolean syn;
	PacketTimer packettime;
	long timerbegin = 0;
	Socket dataSocket;
	long eerder;
	HashMap<Integer, byte[]> packetList = new HashMap<Integer, byte[]>();
	HashMap<Integer, TransportPacket> incomingPacketList = new HashMap<Integer, TransportPacket>();
	Transport transport;

	ControlConnection controller;

	/**
	 * Creates a TransportConnection, indicating a connection between two
	 * applications, on Tranport layer level.
	 * 
	 * @param incoming
	 *            the incoming TPA, used by the application itself
	 * @param outgoing
	 *            the outgoing TPA, used by the other side of the connection and
	 *            used to identify the other side
	 * @param inport
	 *            the incoming port, used by the application
	 * @param outport
	 *            the outgoing port, used by the other side
	 * @param t
	 *            the implementation of the Transport layer.
	 * @param c
	 *            the Controlconnection, defines the flow of controls sent by
	 *            the user.
	 */
	public TransportConnection(TPA incoming, TPA outgoing, byte inport,
			byte outport, Transport t, ControlConnection c) {
		incomingport = inport;
		outgoingport = outport;
		in = incoming;
		out = outgoing;
		transport = t;
		controller = c;
		seq = 0;
		lastReceivedAck = 0;
		sendBase = 0;
		
		dataSocket = new Socket();
	}

	/**
	 * 
	 * @return the port on the other side
	 */
	public int getOutPort() {
		return outgoingport;
	}

	/**
	 * 
	 * @return the port of the application
	 */
	public int getInPort() {
		return incomingport;
	}

	/**
	 * 
	 * @return the TPA of the application
	 */
	public TPA getIncomingAdresst() {
		return in;
	}

	/**
	 * import network.TPA;
	 * 
	 * @return theint lastReceivedAck TPA of the other side
	 */
	public TPA getOutgoingAdresst() {
		return out;
	}

	/**
	 * 
	 * @return the current sequence number of sent packets, this is not the last
	 *         acked sequence number
	 */
	public int getSeq() {
		return seq;
	}

	/**
	 * 
	 * @return the current sequence number of ack's. This shows how many packets
	 *         are received.
	 */
	public int getAckSeq() {
		return ackseq;
	}

	/**
	 * 
	 * @param ds
	 *            the socket through which data flows
	 */
	public void setDataSocket(Socket ds) {
		dataSocket = ds;
	}

	/**
	 * Used by Go back-N. Retrieves a datapacket, using the corresponding
	 * sequence number.
	 * 
	 * @param seq
	 *            the sequence number required to find the right datapacket
	 * @return the next packet after the last-acked packet
	 */
	public byte[] getDataFromMap(int seq) {
		return packetList.get(seq);
	}

	/**
	 * Handles incoming packets from the network layer. First checks whether the
	 * packet sent is a FIN, then a SYN. If a FIN occurs, checks whether it is a
	 * FINACK or not. If FINACK is the case, closes the connections, otherwise
	 * it first sends a FINACK and then waits until nothing is send for a while.
	 * Then it closes the connection. If a SYN occurs, checks whether it is a
	 * SYNACK or not. If SYNACK is the case, opens the connection, otherwise it
	 * first sends a SYNACK and opens the connection.
	 * 
	 * When neither is the case. First the packet is checked for its seq and
	 * ackseq. Then the ack and data that should follow on those two numbers is
	 * sent to the other connection.
	 * 
	 * @param packet
	 *            the TransportPacket, forwarded by the Transport layer.
	 * @throws IOException
	 */

	public void connected() {
		syn = true;
	}

	public void handle(TransportPacket packet) throws IOException {
		System.out.println("Bericht ontvangen met seq: " + packet.getSeq());
		System.out.println("Bericht ontvangen met ack: " + packet.getAckseq());
		// als fin dan FINACK of SLUITEN
		if (packet.isFin()) {

			// als FINACK dan sluiten van verbinding en controller laten sluiten
			if (packet.isAck()) {
				controller.disconnected();
				syn = false;
				
				
				packetList.clear();
				incomingPacketList.clear();
			} else {
				// als FIN dan stuur FINACK en controller laten sluiten
				transport.quitack(this, out);
				controller.disconnected();
				syn = false;
				
				packetList.clear();
				incomingPacketList.clear();
			}

		} else
		// als data dan DATA naar IPL en CHECKSEQ en ACK
		if (packet.getData().length != 0) {

			// data wordt in IPL gestopt
			incomingPacketList.put((int) packet.getSeq(), packet);
			// als de seq verwacht is (dus niet groter)

			if (packet.getSeq() == ackseq) {
				boolean gapFound = false;
				// loop tot er een 'gat' in de ontvangen data zit
				for (int i = packet.getSeq(); !gapFound; i = (i + 1)
						% Constants.MAX_SEQUENCE_NUMBER) {

					// check of volgende nummer in IPL zit
					if (incomingPacketList.containsKey(i)) {
						// stuur data uit IPL naar app
						dataSocket.getOutputStream().write(
								incomingPacketList.get(i).getData());
						dataSocket.getOutputStream().flush();
						incomingPacketList.remove(i);
						if(i==0){
							System.out.println("Speed is: " +(Constants.MAX_PACKET_LENGTH*Constants.MAX_SEQUENCE_NUMBER)/(System.currentTimeMillis()-eerder) + "b/s");
							eerder = System.currentTimeMillis();
						}
					} else {
						// gat gevonden, beeindig loop en zet de ackseq op het
						// volgend verwachte stuk
						gapFound = true;
						ackseq = (char) i;

					}
				}
			}
			ack();
		}
		// als syn dan SYNACK of ACK/DATA
		if (packet.isSyn()) {
			if (packet.isAck()) {
				// als SYNACK dan wordt de controller verteld dat er een
				// verbinding is en syn op true gezet
				controller.connected();
				ack();
				syn = true;
			} else {
				// als SYN dan wordt de controller verteld dat er een verbinding
				// is en wordt een SYnACK gestuurd


				transport.connectack(this, out);

				syn = true;
			}

		} else
		// als ack dan CHECKSEQ en evt. DATA
		if (packet.isAck() && !packet.isFin()) {
			// Checken of ack al ontvangen is

			if ((int) packet.getAckseq() == lastReceivedAck) {
				resendCountdown++;
				// Als dezelfde ack drie keer is ontvangen, wordt een resend
				// gestuurd.
				if ((resendCountdown > 3
						&& System.currentTimeMillis() - timerbegin > Constants.TIMEOUT_VALUE) //TODO moet dit een || ipv && zijn?
						&& resendCounter < Constants.DISCONNECT_THRESHOLD) {
					timerbegin = System.currentTimeMillis();
					System.out.println("Duplicate ACK: " + lastReceivedAck);
					try {
						data(packetList.get(lastReceivedAck), lastReceivedAck);
					} catch (NullPointerException e) {

					}
					// resend counter wordt meer
					resendCounter++;
					resendCountdown = 0;
				}
				// zodra 5 keer is verzonden, disconnect. Blijkbaar is de
				// ontvanger er niet meer.
				if (resendCounter >= Constants.DISCONNECT_THRESHOLD) {
					System.out.println("Ik disconnect!");
					
					controller.disconnect();
					syn = false;
				}
			} else {
				// reset de resendCountdown en timer
				resendCountdown = 0;
				timerbegin = System.currentTimeMillis(); // TODO Moet dit niet System.getCurrentTimeMillis(); zijn?
				resendCounter = 0;
			}
			// verwijderen packets van laatst ontvangen ack tot nieuwe ack
			for (int i = lastReceivedAck; i < packet.getAckseq(); i = (i + 1)
					% Constants.MAX_SEQUENCE_NUMBER) {
				
				packetList.remove(i);
				
			}
			// nieuwe ack is laatst ontvangen
			lastReceivedAck = packet.getAckseq();
//			System.out.println("Last received ACK is " + lastReceivedAck);
			// Data sturen zou evt. nog mogen, maar deze implementatie wordt pas
			// later bekeken, aangezien het voor de werking van de Transportlaag
			// weinig uitmaakt.
		}

	}

	/**
	 * Sends a packet of data, without any ACK's, SYN's or FIN's
	 * 
	 * @param bs
	 *            data to be sent
	 * @throws IOException
	 */
	public void data(byte[] bs, int sequence) throws IOException {
		transport.data(this, out, bs, sequence);

	}

	/**
	 * sends a SYN packet, requesting for a connection to be set up.
	 * 
	 * @throws IOException
	 */
	public void sendsyn() throws IOException {
		transport.connect(this, out);
	}

	/**
	 * sends an ACK, without data. Sends a packet to confirm data has been
	 * received.
	 * 
	 * @throws IOException
	 */
	public void ack() throws IOException {
		transport.ack(this, out);

	}

	/**
	 * Sends an ACK and sends some data too. Sends a confirmation of received
	 * packets.
	 * 
	 * @param data
	 *            the data that has to be sent.
	 * @throws IOException
	 */
	public void ackdata(byte[] data, int sequence) throws IOException {

		transport.ack(this, out, data, sequence);

	}

	/**
	 * Sends a FIN packet, requesting the// TODO Auto-generated catch block
	 * e.printStackTrace(); closing of a connection. This causes the connection
	 * to wait for a FINACK.
	 * 
	 * @throws IOException
	 */
	public void quit() throws IOException {
		transport.quit(this, out);

	}

	public void run() {
		// inputstream wordt gemaakt
		eerder = System.currentTimeMillis();
		boolean put = false;
		// probeer altijd de inputstream van de app te lezen
		while (syn) {
			byte[] in = new byte[Constants.MAX_PAYLOAD_LENGTH];
			byte[] inNonStuffed = new byte[Constants.MAX_PAYLOAD_LENGTH];
			put = false;
			try {
				// lees de inputstream van de datasocket en stop het in in.
				int length;
				try{
				length = dataSocket.getInputStream().read(in, 0,
						Constants.MAX_PAYLOAD_LENGTH);
				
				if (length < Constants.MAX_PAYLOAD_LENGTH && length >= 0) {
					inNonStuffed = new byte[length];
					for (int i = 0; i < length; i++) {
						inNonStuffed[i] = in[i];
					}
				} else {
					inNonStuffed = in;
				}}catch(SocketException e){
					System.out.println("The connection of the sockets was closed");
					syn = false;
					packettime.dispose();
					
					quit();
					packetList.clear();
			
				
				}

			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// als je data nog niet in de packetlist zit, wordt deze loop
			// geloopt, zodat als de buffer van data vol zit, de data niet
			// verloren gaat. Dit zou oneindig kunnen, of totdat de verbinding
			// wordt verbroken
			//System.out.println("Ik ga data versturen!");
			while (!put) {
				// als de sendBase (seq van nieuw stuk data) niet in de PL zit
				if (packetList.size() < Constants.WINDOW) {
					// stop de instream vanaf app in de PL
					packetList.put(sendBase, inNonStuffed);
					try {
						// als er een verbinding is
						if (syn) {
							// stuur de data direct door naar de ontvanger
							if(packetList.get(sendBase).length >0){
							data(packetList.get(sendBase), sendBase);
							}
						}
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					long sentTime = System.currentTimeMillis();
					// als de timer nog niet gestart is
					if (syn) {
						new PacketTimer(sentTime, this,inNonStuffed,sendBase).start();
						
						timerStarted = true;
						

					}
					//System.out.println("Nieuwe data aangeleverd met seq = "
					//		+ sendBase);
					// verhoog de sendBase met 1, met een limiet van het
					// maximumsequencenummer
					sendBase = (sendBase + 1) % Constants.MAX_SEQUENCE_NUMBER;
					// zeg dat het pakket geplaatst is
					put = true;

				}
				// als het niet gelukt is de data te plaatsen
				if (!put) {
					try {
						// wacht 1 milliseconde, probeer daarna weer
						Thread.sleep(1);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
		
	}

	public boolean isSameConnection(int inport, int outport, TPA inTPA,
			TPA outTPA) {
		return ((inport == outgoingport) && (outport == incomingport)
				&& (inTPA.equals(out)) && (outTPA.equals(in)));
	}

	public void setOutgoingTPA(TPA outTPA) {
		out = outTPA;
	}

	public void setOutgoingPort(byte outPort) {
		outgoingport = outPort;
	}

	public HashMap<Integer, byte[]> getPacketList() {
		return packetList;
	}

	public int getSendBase() {
		return sendBase;
	}

	public int getLastReceivedACK() {
		return lastReceivedAck;
	}

	public void timerStopped() {
		timerStarted = false;
	}

	public boolean isConnected() {
		// TODO Auto-generated method stub
		return syn;
	}
}
